package render;

import java.awt.Color;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.Random;

import javax.swing.JPanel;

import materials.GlassMaterial;
import materials.Material;
import materials.PlainMaterial;
import math.Matrix;
import math.Vector3D;
import objects.Maze;
import objects.Plane;
import objects.RenderObject;
import objects.Sphere;

public class Tracer {

    private Vector3D[] directions;
    private Vector3D[] colorcache;
    private boolean[] done;
    private ArrayList<RenderObject> objects;
    private BufferedImage result;
    private int screenX;
    private int screenY;
    private Random random = new Random();
    public int quality;
    private JPanel panel;
    private int i = 1;

    private Matrix rotation;

    Vector3D startPos = new Vector3D( 0 , 0 , 0 );

    private double light = 0.5;
    private double[] samples;

    public double startx = -50;

    public void increaseLight( double num ) {
	setLight( getLight() + num );
	repaint();
    }

    public BufferedImage getImage() {
	try {
	    Thread.sleep( 50 );
	} catch ( InterruptedException e ) {
	    // TODO Auto-generated catch block
	    e.printStackTrace();
	}
	int u = 0;
	return result;
    }

    public void rotate( double anglex , double angley , double anglez ) {

	Matrix rotx = new Matrix( 1 , 0 , 0 , 0 , Math.cos( anglex ) , -Math.sin( anglex ) , 0 , Math.sin( anglex ) , Math.cos( anglex ) );

	Matrix roty = new Matrix( Math.cos( angley ) , 0 , Math.sin( angley ) , 0 , 1 , 0 , -Math.sin( angley ) , 0 , Math.cos( angley ) );
	Matrix rotz = new Matrix( Math.cos( angley ) , 0 , Math.sin( angley ) , 0 , 1 , 0 , -Math.sin( angley ) , 0 , Math.cos( angley ) );

	// yaw Matrix roty = new Matrix(Math.cos( angley ),-Math.sin( angley
	// ),0,Math.sin( angley ),Math.cos( angley ),0,0,0,1);
	rotation = rotx.mult( roty );

    }

    public void move( double x , double y ) {
	Vector3D dir = directions[screenX / 2 + screenY / 2 * screenX];
	startPos = startPos.add( rotation.mult( dir.mult( y ) ) );

	startPos = startPos.add( rotation.mult( new Vector3D( dir.z , dir.y , dir.x ).mult( x ) ) );

	clearBuffers();
    }

    public void resetSize( int x , int y ) {
	result = new BufferedImage( x , y , BufferedImage.TYPE_INT_RGB );
	setScreenX( x );
	setScreenY( y );

	done = new boolean[screenX * screenY];
	colorcache = new Vector3D[screenX * screenY];
	samples = new double[screenX * screenY];
	for ( int q = 0; q < screenX * screenY; q++ ) {
	    colorcache[q] = new Vector3D( 0 , 0 , 0 );
	    samples[q] = 0;
	}
	initRays();
    }

    public Tracer( int x , int y , int quality , JPanel panel ) {
	rotation = new Matrix( 1 , 0 , 0 , 0 , 1 , 0 , 0 , 0 , 1 );
	this.panel = panel;
	this.quality = quality;
	result = new BufferedImage( x , y , BufferedImage.TYPE_INT_RGB );
	objects = new ArrayList<RenderObject>();

	setScreenX( x );
	setScreenY( y );
	colorcache = new Vector3D[screenX * screenY];
	samples = new double[screenX * screenY];

	done = new boolean[screenX * screenY];
	for ( int q = 0; q < screenX * screenY; q++ ) {
	    colorcache[q] = new Vector3D( 0 , 0 , 0 );
	    samples[q] = 1;
	    done[q] = true;
	}
	initRays();
	for ( int i = 0; i < 30; i++ ) {
	    objects.add( new Sphere( new Vector3D( random.nextInt( 25000 ) - 12500 , 50 , random.nextInt( 25000 ) - 12500 ) , new Material(
		    new Vector3D( Math.random() , Math.random() , Math.random() ) , 0.5 , 0 , false ) , 880 ) );
	}

	objects.add( new Sphere( new Vector3D( 0 , 1001000 , 650 ) , new Material( new Vector3D( 1 , 1 , 1 ) , 0.5 , 0 , false ) , 1000000 ) );
	// objects.add( new Plane( new Vector3D( -50 , 150 , -50 ) , new
	// Vector3D( 0 , -1 , 0 ) , new PlainMaterial() ) );
    }

    public void initRays() {
	directions = new Vector3D[getScreenY() * getScreenX()];
	for ( int y = 0; y < getScreenY(); y++ ) {
	    for ( int x = 0; x < getScreenX(); x++ ) {
		Vector3D currDirection = new Vector3D();
		currDirection.setX( x - (getScreenX() / 2) );
		currDirection.setY( y - (getScreenY() / 2) );
		currDirection.setZ( 14 + 60 * ((getScreenX() * getScreenY()) / 100000) );
		currDirection.Normalize();
		directions[x + y * getScreenX()] = currDirection;
	    }
	}

    }

    private RenderResult trace( Ray ray ) {
	double close = 1000000;
	RenderObject closest = null;
	for ( RenderObject object : objects ) {
	    TraceResult result = object.trace( ray );
	    if ( result.isHit() ) {
		if ( result.getDistance() < close ) {
		    close = result.getDistance();
		    closest = object;
		}
	    }
	}
	return new RenderResult( closest , close );
    }

    private Vector3D recursiveRender( Ray raydo , int depth ) {
	Ray ray = raydo;
	// ray.setDirection( ray.getDirection().add(
	// Vector3D.randomVector().mult( 0.1 ) ) );
	// ray.getDirection().Normalize();
	RenderResult foo = trace( ray );
	depth--;
	if ( depth <= 0 )
	    return new Vector3D( 0 , 0 , 0 );

	if ( foo.object == null ) {
	    double sky = ray.getDirection().dot( new Vector3D( 0 , -1 , 0 ) ) * 2;

	    return new Vector3D( sky , sky + 0.5 , sky + 0.5 );
	}

	Vector3D intersect = ray.getPosition().add( ray.getDirection().mult( foo.distance ) );
	Vector3D normal = foo.object.getNormal( intersect );

	Vector3D Ran = new Vector3D( (random.nextDouble() - 0.5) * 2 , (random.nextDouble() - 0.5) * 2 , (random.nextDouble() - 0.5) * 2 );
	Ran.Normalize();
	//
	if ( foo.object.getMaterial().isGlass() ) {
	    Vector3D newDirection = ray.getDirection();
	    double fresnel = normal.dot( ray.getDirection() );
	    Vector3D reflect = normal.add( ray.getDirection().mult( fresnel ) );
	    newDirection = reflect;

	    newDirection.Normalize();

	    Ray continueRay = null;

	    if ( Math.random() > (fresnel * -1) ) {
		continueRay = new Ray( intersect , newDirection );
		continueRay.getDirection().Normalize();
		Vector3D bounceColor = recursiveRender( continueRay , depth );
		return bounceColor;
	    }
	}

	Ray continueRay = new Ray( intersect , normal.add( Ran.mult( 0.95 ) ) );
	continueRay.getDirection().Normalize();
	double cost = continueRay.getDirection().dot( normal );

	Vector3D bounceColor = recursiveRender( continueRay , depth );
	// foo.object.getMaterial().getColor().mult(emmit).add(
	Vector3D retColor = foo.object.getMaterial().getColor().mult( foo.object.getMaterial().getEmmit() )
		.add( (bounceColor.mult( cost )).minus( (new Vector3D( 1 , 1 , 1 ).minus( foo.object.getMaterial().getColor() )) ) );

	return retColor;
    }

    private double rot = 0;

    // renders 50 random pixels, then repaints
    public void renderRandom() {
	for ( int i = 0; i < 4000; i++ ) {
	    int x = random.nextInt( screenX );
	    int y = random.nextInt( screenY );

	    Vector3D dir = directions[x + y * getScreenX()];

	    dir.Normalize();
	    Vector3D b = rotation.mult( dir );
	    b.Normalize();
	    Ray start = new Ray( startPos , b );

	    Vector3D vDisturbance = new Vector3D();
	    vDisturbance.x = 20.5f * random.nextDouble();
	    vDisturbance.y = 20.5f * random.nextDouble();
	    vDisturbance.z = 20.5f * random.nextDouble();

	    start.setPosition( start.getPosition().add( vDisturbance ) );

	    Vector3D n = recursiveRender( start , 30 );

	    if ( done[x + y * screenX] ) {
		    colorcache[x + y * screenX] = new Vector3D( 0 , 0 , 0 );
		    samples[x + y * screenX] = 0;
		    done[x + y * screenX] = false;
		}
	    
	    samples[x + y * screenX]++;
	    colorcache[x + y * screenX] = colorcache[x + y * screenX].add( n );
	}
	repaint();
	panel.repaint();
    }

    public void render() {
	i++;
	long time = System.nanoTime();
	for ( int x = 0; x < getScreenX(); x++ ) {
	    if ( x % 25 == 0 ) {
		repaint();
		panel.repaint();
	    }
	    long timeg = System.nanoTime() - time;

	    time = System.nanoTime();
	    // System.out.println( x / (getScreenX() / 100) + " % done" );
	    // System.out.println( "Time for 1 line:" + ((double) timeg) /
	    // 1000000000 + " sec" );
	    // System.out.println( "Expected total time:" + (((double) timeg) /
	    // 1000000000) * (screenX - x) + " sec" );
	    for ( int y = 0; y < getScreenY(); y++ ) {
		Vector3D dir = directions[x + y * getScreenX()];
		dir.Normalize();

		// RenderResult foo = trace( start );
		// double dist = Math.abs((foo.distance)/10000);
		// Vector3D rand = dir.add( Vector3D.randomVector().mult(0.9) );
		// rand.Normalize();
		// dir = dir.add( rand.mult( dist ) );
		// dir.Normalize();
		Ray start = new Ray( startPos , dir );

		Vector3D n = recursiveRender( start , 3 );
		// System.out.println (n.getX() + " "+ n.getY() + " "+
		
		samples[x + y * screenX]++;
		colorcache[x + y * screenX] = colorcache[x + y * screenX].add( n );

	    }
	}
    }

    public void repaint() {
	// System.out.println(light);
	/*
	 * Vector3D maxlight = new Vector3D( 0 , 0 , 0 ); for ( int x = 0; x <
	 * getScreenX(); x++ ) { for ( int y = 0; y < getScreenY(); y++ ) {
	 * Vector3D nope = colorcache[x + y * screenX].clone(); nope = nope.div(
	 * i ); if ( nope.getX() > maxlight.getX() ) maxlight.setX( nope.getX()
	 * ); if ( nope.getY() > maxlight.getY() ) maxlight.setY( nope.getY() );
	 * if ( nope.getZ() > maxlight.getZ() ) maxlight.setZ( nope.getZ() ); }
	 * }
	 */
	for ( int x = 0; x < getScreenX(); x++ ) {
	    for ( int y = 0; y < getScreenY(); y++ ) {

		Vector3D no = colorcache[x + y * screenX];

		no = no.div( samples[x + y * screenX] );
		no = no.mult( light );
		// no.setX( no.getX() / maxlight.getX() );
		// no.setY( no.getY() / maxlight.getY() );
		// no.setZ( no.getZ() / maxlight.getZ() );

		// :)

		if ( no.getX() > 1.0 )
		    no.setX( 1.0 );
		if ( no.getY() > 1.0 )
		    no.setY( 1.0 );
		if ( no.getZ() > 1.0 )
		    no.setZ( 1.0 );
		if ( no.getX() < 0.0 )
		    no.setX( 0.0 );
		if ( no.getY() < 0.0 )
		    no.setY( 0.0 );
		if ( no.getZ() < 0.0 )
		    no.setZ( 0.0 );

		Color newColor = new Color( 1 , 1 , 1 );
		try {
		    newColor = new Color( (int) (no.getX() * 250) , (int) (no.getY() * 250) , (int) (no.getZ() * 250) );
		} catch ( IllegalArgumentException e ) {
		    System.out.println( no.getX() + " " + no.getY() + " " + no.getZ() );
		}

		result.setRGB( x , y , newColor.getRGB() );
	    }
	}

    }

    public void clearBuffers() {
	for ( int q = 0; q < screenX * screenY; q++ ) {
	    done[q] = true;
	}
    }

    public void setScreenX( int screenX ) {
	this.screenX = screenX;
    }

    public int getScreenX() {
	return screenX;
    }

    public void setScreenY( int screenY ) {
	this.screenY = screenY;
    }

    public int getScreenY() {
	return screenY;
    }

    public void setLight( double light ) {
	this.light = light;
    }

    public double getLight() {
	return light;
    }

}
